Skip to content

fix(app-shell): forward the authored chart block WHOLE to ListView, not a six-key projection (objectui#7823) - #7890

Merged
os-sam merged 1 commit into
mainfrom
claude/issue-7823-objectview-chart-relay-projection
Sep 6, 2026
Merged

fix(app-shell): forward the authored chart block WHOLE to ListView, not a six-key projection (objectui#7823)#7890
os-sam merged 1 commit into
mainfrom
claude/issue-7823-objectview-chart-relay-projection

Conversation

@claude

@claude claude Bot commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

Fixes #7823

ObjectView's renderListView relay handed ListView a hand-listed projection of
exactly six keys of the authored chart: block. This replaces that copy with a pointer.

What was measured, before any code was touched

The card was written at 900f8d995 and flagged three things as unverified or
carried-over. All three were re-read line by line on origin/main 83fe6e741.

1. The line numbers still hold. The dedicated chart-view branch is still at :2098,
the fullSchema literal still opens at :2170, and the six-key chart: projection was
still at :2435.

2. InterfaceListPage does NOT carry the same projection — and the card's "maybe it
does not relay it at all" is the closer half of its guess.
It never names chart, and
it hand-lists nothing: it forwards the referenced view's options bag WHOLE
(options: { ...(view.options ?? {}), ...(mapCfg ? { map: mapCfg } : {}) }, :477), so
a chart block authored under options passes through untouched. It relays no
view-level chart: key at all — but it does relay the view-level map block verbatim
one line above (...((view as any).map ? { map: (view as any).map } : {})), which is the
in-repo precedent for the shape this PR writes. The six-key projection is unique to
ObjectView.tsx
, so this fix is correctly scoped to one file and no second host needs
the same edit.

3. The spread order — and a CORRECTION to the card. The card asserts that
schema.chart at the ListView end "is the object-level list's block, never the active
view's". The first half of that is right as a mechanism and wrong as a reading:

  • fullSchema does spread ...listSchema first (:2171) and declares no top-level
    chart: rung, so nothing overrides whatever the spread carries.
  • But the listSchema this relay receives is built by plugin-view's ObjectView
    (:1747), and that literal has no chart key at all. So schema.chart at the
    ListView end is undefined on this route — not an object-level chart block. There is
    no object-level block to shadow the view's, because none is ever constructed.
  • The six-key projection is nested inside options: (:2371), not at the top level of
    fullSchema, so what the gate actually consulted was schema.options.chart.

Net effect on the gate, which resolves schema.chart || schema.options?.chart || {}:
undefined || sixKeyHusk — the husk won every time, because an object of six
undefined values is truthy. The card's conclusion (the ADR-0021 shape never arrives)
is correct; its route is not, and only the projection needed fixing.

The fix: a pointer, not a wider copy

chart: viewDef.chart,

Widening the list from six keys to nine would buy ADR-0021's correctness and re-arm the
identical trap for the next block key, with nothing to fire when it happens: viewDef is
an untyped record (Record of string to any), so a missing rung is invisible to tsc (#7559 owns that mechanism
and explicitly disclaims this census). A hand-listed key set is a copy of a schema's key
set, and copies rot silently.

The whole relay carries no key ListView can trip over, and this was checked rather
than assumed
(the ruling's stop-and-report condition). ListView never SPREADS this
block: resolveListChartBinding and case 'chart' both read it BY NAME — dataset,
dimensions, values, chartType, xAxisField, yAxisFields, categoryField,
valueField, aggregation, series. Keys it does not consult are ignored exactly as
they already are for the gantt / timeline / tree blocks in the same literal, which
are relayed whole today. No stop-and-report condition was met.

One shape change worth naming: an undeclared block now forwards undefined instead of a
permanently-truthy husk of six undefined keys. Both resolve to "no binding" at the gate,
so the observable answer is unchanged — and the negative controls below pin that.

Behaviour evidence — a reading BEFORE and AFTER

New test packages/app-shell/src/views/ObjectView.chartRelay-7823.test.tsx mounts the
real ListView behind the real relay (the sibling relay tests stub it; this card's claim
is about the capability gate one seam further on, so the assertion is the Chart toggle in
the DOM, per the #6318 standard that it renders differently).

before (83fe6e741, unmodified) after
ADR-0021 view (dataset/dimensions/values), whitelisting ['grid','chart'] no Chart toggle (expected false to be true) Chart toggle offered
options.chart handed to the gate {chartType, xAxisField, yAxisFields, aggregation, series, config}, all undefined the authored block, toEqual verbatim
legacy categoryField / valueField no toggle toggle offered
CONTROL legacy xAxisField / yAxisFields toggle offered toggle offered (unchanged)
NEGATIVE CONTROL: view declares nothing no toggle no toggle
NEGATIVE CONTROL: empty block chart: {} no toggle no toggle
NEGATIVE CONTROL: chart: { chartType: 'pie' }, no binding no toggle no toggle

The gate under test is the live one, not a stale build: the root vitest config aliases
@object-ui/plugin-list to packages/plugin-list/src, so the ListView that rendered
here is current source carrying #7544's capability check.

Measured run counts: before 5 failed / 3 passed; after 8 passed. Four of the five
failures are the THE FIX arms; the fifth is the SCHEMA half of the first negative
control (options.chart was the husk, not absent) — its DOM half, the toggle, was green
before and stayed green after. Every negative control's behaviour assertion is green in
both worlds, which is what refuses a "fix" that merely widens the husk and lights the
toggle on views that configured no chart.

What this is not

Not #7559 (that card owns the mechanism and disclaims the census; this is a rung that
EXISTS and narrows, which a rung census cannot report because the key is present). Not
#7547 / #7029 / #7070 — those are invented binding names on the FORCED path; nothing here
adds or removes a floor. Not #7544 itself, which is packages/plugin-list and lands the
gate this relay now feeds. packages/plugin-list/ and packages/types/ are untouched.

Gates, re-run on the pushed commit 4fed69acc with git diff HEAD empty

  • node scripts/check-changeset-presence.mjs — EXIT=0, verbatim: 2 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s): .changeset/7823-objectview-chart-relay-whole-block.md. Scored patch.
  • node scripts/check-changeset-no-major.mjs — EXIT=0, No changeset declares a major bump.
  • node scripts/check-governed-queue-guard.mjs --test over the three changed paths —
    EXIT=0, verbatim: NOT GOVERNED - 3 path(s) checked against 5 governed surface(s); none matched. An ordinary pull request: the normal review and merge-queue route applies.
  • pnpm check:control-bytes, check:vi-mock-specifiers, check:vi-mock-inherit,
    check:governed-queue-guard — all EXIT=0.
  • pnpm --filter @object-ui/app-shell type-check — EXIT=0. The new test file's presence
    in that program was confirmed with --listFiles (1 hit) rather than assumed, since the
    emitting tsconfig excludes test directories.
  • pnpm exec vitest run packages/app-shell/src/views/ (the changed file's whole
    directory, recursive) — EXIT=0, Test Files 376 passed (376),
    Tests 3580 passed | 1 skipped (3581). No sibling relay or view test regressed.
  • pnpm --filter @object-ui/app-shell lint — EXIT=0, 0 errors.
  • node scripts/check-lint-coverage.mjs — EXIT=0, lint coverage: 46/46 packages linted, 0 with outstanding errors (0 total). (Its first run through pnpm -s exited 254 with
    EMPTY output while the test suite held memory — read as "could not run", not as a
    finding, and re-measured directly.)
  • pnpm check:unreferenced-sources, check:handler-key-reads, check:i18n-keys — all
    EXIT=0.

Exit codes were captured by redirecting first, never through a pipe.


🤖 Generated with Claude Code

https://claude.ai/code/session_01KbJQ1y1J12nZxYzFWhP8Q3


Generated by Claude Code

…bjectui#7823)

The object-view relay handed `ListView` a hand-listed projection of exactly six
keys — `chartType`, `xAxisField`, `yAxisFields`, `aggregation`, `series`,
`config` — which is the pre-ADR-0021 key set, frozen. The whole ADR-0021
(objectui#1890) authoring shape (`dataset` / `dimensions` / `values`) and the
legacy `categoryField` / `valueField` spelling had no rung, so a view that
declared them reached `ListView` with its binding stripped and `ListView` could
not tell that from a view that declared nothing.

Once objectui#7544 gave `ListView.availableViews` a chart capability check, the
gate was handed six `undefined` keys and correctly answered "nothing declared"
about a view whose author declared everything: ADR-0047 filtered the author's
own whitelist down to `['grid']`, so no Chart toggle appeared and no diagnostic
was emitted. The legacy `xAxisField` spelling did survive the projection and did
resolve, so the two authoring shapes behaved differently on this route for
reasons that lived entirely in that one object literal.

The projection is replaced by a pointer, not widened from six keys to nine: a
hand-listed key set is a copy, and copies rot silently — three more keys would
buy this ADR's correctness while re-arming the identical trap for the next block
key, with nothing to fire, because `viewDef` is `Record<string, any>` and a
missing rung is invisible to tsc (objectui#7559 owns that mechanism and
disclaims this census). Forwarding whole is safe because `ListView` reads the
block BY NAME at both readers — `resolveListChartBinding` and `case 'chart'` —
and never spreads it, so no key this relay stops dropping can collide
downstream. It is the shape the sibling `gantt` / `timeline` / `tree` blocks in
the same literal already use.

An undeclared block now forwards `undefined` rather than the old
permanently-truthy husk of six `undefined` keys, so the gate still offers no
Chart toggle to a view that configured none.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KbJQ1y1J12nZxYzFWhP8Q3
@github-actions

github-actions Bot commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 50 chunks) 3187.2 KB 3191.4 KB
Main entry chunk (gzip) 143.4 KB 350 KB
Entry file index-_j3hGM02.js
Status PASS

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 15.67KB 5.75KB
app-shell (runtime-config.js) 20.68KB 7.36KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.06KB 3.86KB
auth (ActiveOrganizationStorage.js) 25.05KB 9.16KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.18KB 10.59KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.65KB 2.22KB
auth (SocialSignInButtons.js) 9.61KB 3.89KB
auth (UserMenu.js) 3.41KB 1.23KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.21KB 10.80KB
auth (createAuthenticatedFetch.js) 8.46KB 3.43KB
auth (index.js) 3.19KB 1.44KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 5.13KB 2.35KB
collaboration (CommentThread.js) 26.08KB 7.56KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 510.60KB 116.20KB
core (index.js) 6.96KB 2.79KB
create-plugin (index.js) 10.08KB 3.26KB
data-objectstack (index.js) 182.08KB 50.62KB
fields (index.js) 242.44KB 61.25KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 1.22KB 0.64KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 4.28KB 1.75KB
i18n (index.js) 3.65KB 1.47KB
i18n (pickLocalized.js) 7.62KB 3.26KB
i18n (provider.js) 26.89KB 9.04KB
i18n (useDisplayLocale.js) 2.85KB 1.45KB
i18n (useObjectLabel.js) 34.34KB 9.17KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 38.98KB 10.98KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.99KB 0.87KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 2.53KB 0.85KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 4.39KB 1.66KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 11.71KB 4.29KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.24KB 2.16KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 5.12KB 1.74KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 15.75KB 3.80KB
plugin-calendar (index.js) 47.87KB 13.31KB
plugin-charts (index.js) 70.92KB 19.75KB
plugin-chatbot (index.js) 196.19KB 46.37KB
plugin-dashboard (index.js) 132.88KB 34.69KB
plugin-designer (index.js) 212.86KB 43.19KB
plugin-detail (index.js) 250.55KB 64.06KB
plugin-editor (index.js) 2.46KB 1.10KB
plugin-form (index.js) 132.87KB 32.66KB
plugin-gantt (index.js) 167.26KB 41.00KB
plugin-grid (index.js) 209.29KB 56.78KB
plugin-kanban (index.js) 52.71KB 14.55KB
plugin-list (index.js) 113.76KB 27.75KB
plugin-map (index.js) 20.44KB 6.78KB
plugin-markdown (index.js) 13.93KB 4.81KB
plugin-report (index.js) 43.59KB 11.97KB
plugin-timeline (index.js) 30.84KB 8.85KB
plugin-tree (index.js) 9.20KB 3.19KB
plugin-view (index.js) 85.24KB 20.94KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.66KB 3.50KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 81.07KB 26.86KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.63KB 2.18KB
react (schema-input.js) 2.32KB 1.24KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (codegen.js) 5.41KB 2.34KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 4.93KB 2.24KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (parse.js) 20.57KB 5.88KB
sdui-parser (provenance.js) 3.66KB 1.82KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 10.35KB 3.60KB
types (ai.js) 0.20KB 0.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 1.00KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 2.74KB 1.41KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 4.74KB 2.25KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 4.73KB 2.28KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 5.05KB 1.93KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.11KB 3.32KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants